小心使用 method swizzling
使用 method swizzling,我们通常会想到一个方法:void method_exchangeImplementations(Method m1, Method m2)
下面我们测一下这个方法。
首先创建一个简单的工程,根控制器是一个 UINavigationController
navi,navi的根控制器是一个ViewController
vc,而ViewController
继承自BaseViewController
,在BaseViewController
里只有一个方法:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
然后我们创建一个BaseViewController
的分类BaseViewController+Swizzling
,这个分类里有如下两个方法:
+ (void)load
{
Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(ext_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)ext_viewWillAppear:(BOOL)animated
{
NSLog(@"self: %@, %s", self, __func__);
[self ext_viewWillAppear:animated];
}
运行工程,crash日志如下:
2019-03-28 11:00:07.205536+0800 TestMethodSwizzling[40022:14367088] self: <UINavigationController: 0x7fefdf01dc00>, -[BaseViewController(Swizzling) ext_viewWillAppear:]
2019-03-28 11:00:21.830011+0800 TestMethodSwizzling[40022:14367088] -[UINavigationController ext_viewWillAppear:]: unrecognized selector sent to instance 0x7fefdf01dc00
2019-03-28 11:00:21.838515+0800 TestMethodSwizzling[40022:14367088] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationController ext_viewWillAppear:]: unrecognized selector sent to instance 0x7fefdf01dc00'
这个crash让人很困惑,因为我们发现,navi 在尝试执行ext_viewWillAppear:
方法时发生了崩溃。现在让我们看看到底发生了什么。
我们知道,程序在开始运行之后,navi 会调用自己的viewWillAppear:
方法,先不用管UINavigationController
有没有 override UIViewController
的 viewWillAppear:
方法,这无关紧要,因为最终 navi 还是要调用它的父类,也就是 UIViewController
的 viewWillAppear:
,这没有问题。但是,UIViewController
的 viewWillAppear:
方法已经不是最初的实现了,因为在BaseViewController+Swizzling
的load
方法被调用之后,它的实现已经变成了:
NSLog(@"self: %@, %s", self, __func__);
[self ext_viewWillAppear:animated];
很多人又困惑了,我交换的是BaseViewController
的 viewWillAppear:
,跟UIViewController
的 viewWillAppear:
有什么关系?有关系!因为现在BaseViewController
并没有 override viewWillAppear:
,也就是说,BaseViewController
自己并没有viewWillAppear:
方法,它用的是它的父类,也就是 UIViewController
的 viewWillAppear:
方法。所以你感觉你交换的是BaseViewController
自己的viewWillAppear:
方法,但是却无意中改变了 UIViewController
中 viewWillAppear:
的方法实现,然后代码执行到[self ext_viewWillAppear:animated];
,控制器发现自己并没有ext_viewWillAppear:
方法,于是就崩溃了。
这个影响是非常大的,因为你项目中所有的控制器都继承自 UIViewController
(包括UINavigationController
),任何一个控制器直接或间接调用 UIViewController
的 viewWillAppear:
都将引起崩溃。现在已经找到崩溃原因了,我们很容易想到一个解决方案:在BaseViewController
中添加viewWillAppear:
方法:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}
运行,OK!但是,这个方案并不保险。如果在一个已经经过了很多人维护的项目中,突然有一天有人发现这块代码什么都没做,删了吧,一运行,crash。找了半天原因,发现是你加了这个自以为是的 method swizzling。事实上,这块代码除了解决 method swizzling 带来的crash问题,的确什么都没做。而实际情况要复杂甚至严重的多,因为有时候可能因为删了某块貌似冗余的代码,测试也OK,但是一上线却经常出现一些莫名其妙的问题。
这就是runtime,你可以任意访问一些私有方法和属性,也可以动态添加、修改甚至删除一些方法和属性,但是如果使用不善,很多问题可能不能在开发和测试阶段立即暴露出来,但是一上线就是一个随时可能爆炸的炸弹。所以,如果你不能很好的理解和掌握runtime,那就尽量不要使用这些奇技淫巧,而是要坚持软件开发的一些基本原则和模式,这才是阳光大道。知道自己该干什么,不该干什么,知道一个类、一个方法该干什么,不该干什么,这是一个哲学问题。
废话不多说,我们说下一个解决方案。既然不能依赖BaseViewController
的viewWillAppear:
方法,那我们就要从自己身上找问题,而不是寄希望于别人。现在删掉BaseViewController
的viewWillAppear:
方法,然后把BaseViewController+Swizzling
中的load
方法修改如下:
+ (void)load
{
SEL originalSel = @selector(viewWillAppear:);
Method originalMethod = class_getInstanceMethod(self, originalSel);
IMP originalImp = method_getImplementation(originalMethod);
SEL swizzledSel = @selector(ext_viewWillAppear:);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
IMP swizzledImp = method_getImplementation(swizzledMethod);
if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
运行,OK!这段代码的意思很明白。class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))
这个方法的意思是尝试给BaseViewController
添加originalSel
方法,且对应的实现是swizzledImp
。如果BaseViewController
自身已存在originalSel
方法,则方法添加失败,且返回NO,这时候直接调用method_exchangeImplementations(originalMethod, swizzledMethod)
进行 method swizzling 即可。如果BaseViewController
自身不存在originalSel
(可能父类存在该方法,但是这里的"自身不存在"表示不考虑父类),则返回YES,方法添加成功,这时候再调用class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod))
,将BaseViewController
的swizzledSel
的方法实现替换为originalImp
,这样就实现了添加并交换方法实现的目的。
正常到这里就结束了,但这并不全面,让我们更进一步。
首先,让BaseViewController
遵守如下协议,但并不实现协议里的方法。
@protocol TestMethodSwizzlingProtocol <NSObject>
@optional
- (void)doSomething;
@end
然后我们修改BaseViewController+Swizzling
里的方法如下:
+ (void)load
{
SEL originalSel = @selector(doSomething);
Method originalMethod = class_getInstanceMethod(self, originalSel);
IMP originalImp = method_getImplementation(originalMethod);
SEL swizzledSel = @selector(ext_doSomething);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
IMP swizzledImp = method_getImplementation(swizzledMethod);
if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)ext_doSomething
{
NSLog(@"self: %@, %s", self, __func__);
[self ext_doSomething];
NSLog(@"never executed");
}
这段代码其实就是对 doSomething
进行 method swizzling。然后我们在 vc 中添加一个 button,点击 button 时调用 [self doSomething]
。运行,点击button,发现程序执行到ext_doSomething
发生了死循环,根本停不下来,NSLog(@"never executed")
将永远不会执行。
分析原因发现,由于BaseViewController
自身并没有实现doSomething
方法,所以在BaseViewController+Swizzling
的load
方法中,originalMethod
和originalImp
都为nil。class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))
成功执行,但是执行class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod))
却不会有任何效果,因为originalImp
为nil。也就是说我们只替换了@selector(doSomething)
的方法实现,却没有改变@selector(ext_doSomething)
对应的方法实现。换而言之,这个时候@selector(doSomething)
和@selector(ext_doSomething)
都指向相同的实现:
NSLog(@"self: %@, %s", self, __func__);
[self ext_doSomething];
NSLog(@"never executed");
有人说在 BaseViewController
里实现doSomething
方法不就行了。正如前面所说,这是一种方案,但不是一种好的方案。但是这个方案很有启发意义,我们可以在BaseViewController+Swizzling
中加一个doSomething
的默认实现placeholder_doSomething
:
- (void)placeholder_doSomething
{
NSLog(@"I am BaseViewController's default implementaion of 'doSomething' method.");
}
然后修改load
中的代码如下:
+ (void)load
{
SEL originalSel = @selector(doSomething);
Method originalMethod = class_getInstanceMethod(self, originalSel);
IMP originalImp = method_getImplementation(originalMethod);
SEL swizzledSel = @selector(ext_doSomething);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
IMP swizzledImp = method_getImplementation(swizzledMethod);
// 判断原方法是否有对应实现
if (!originalMethod || !originalImp) {
SEL placeholderSel = @selector(placeholder_doSomething);
Method placeholderMethod = class_getInstanceMethod(self, placeholderSel);
IMP placeholderImp = method_getImplementation(placeholderMethod);
BOOL succ = class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
if (succ) {
assert(placeholderMethod && placeholderImp);
class_replaceMethod(self, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
}
else {
NSAssert(0, @"something goes wrong!");
}
return;
}
if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
也就是说,如果BaseViewController
没有实现doSomething
方法,@selector(ext_doSomething)
将与@selector(placeholder_doSomething)
进行方法交换;反之,@selector(ext_doSomething)
将与@selector(doSomething)
进行方法交换。
运行,点击button,当BaseViewController
没有实现doSomething
方法时,[self ext_doSomething]
将执行placeholder_doSomething
里的代码;反之,[self ext_doSomething]
将执行doSomething
里的代码。到此,问题解决。为了方便复用,我们从load
的代码中抽出一个通用方法:
void swizzleInstanceMethod(Class theClass, SEL originalSel, SEL swizzledSel, SEL placeholderSel)
{
Method originalMethod = class_getInstanceMethod(theClass, originalSel);
IMP originalImp = method_getImplementation(originalMethod);
Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSel);
IMP swizzledImp = method_getImplementation(swizzledMethod);
Method placeholderMethod = class_getInstanceMethod(theClass, placeholderSel);
IMP placeholderImp = method_getImplementation(placeholderMethod);
// 判断原方法是否有对应实现
if (!originalMethod || !originalImp) {
BOOL succ = class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
if (succ) {
assert(placeholderMethod && placeholderImp);
class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
}
else {
NSLog(@"something goes wrong!");
assert(0);
}
return;
}
if (class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(theClass, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
修改load
中的代码如下:
+ (void)load
{
SEL originalSel = @selector(doSomething);
SEL swizzledSel = @selector(ext_doSomething);
SEL placeholderSel = @selector(placeholder_doSomething);
swizzleInstanceMethod(self, originalSel, swizzledSel, placeholderSel);
}
至此,OK!
但是还没有结束,我们要再进一步!
假如UIViewController
中有一个方法doNotSuperCallMe
:
- (void)doNotSuperCallMe
{
NSLog(@"self: %@, %s", self, __func__);
assert(0);
}
这个方法,子类可以 override,但是子类不能调用[super doNotSuperCallMe]
,否者将导致崩溃。现在我们的BaseViewController
不确定未来是否要 override 这个方法,但是我们想要在BaseViewController+Swizzling
中 hook 这个方法,代码如下:
+ (void)load
{
SEL originalSel = @selector(doNotSuperCallMe);
SEL swizzledSel = @selector(ext_doNotSuperCallMe);
swizzleInstanceMethod(self, originalSel, swizzledSel, nil);
}
- (void)ext_doNotSuperCallMe
{
NSLog(@"self: %@, %s", self, __func__);
[self ext_doNotSuperCallMe];
}
然后修改 vc 中的 button 为点击时调用[self doNotSuperCallMe]
,并且此时的BaseViewController
和ViewController
中并没有override doNotSuperCallMe
方法。好了,看起来没什么问题,运行,点击button,发现代码执行到了UIViewController
的doNotSuperCallMe
方法而发生了崩溃。这不是我们想要的结果,我们不想调用父类,也就是UIViewController
的doNotSuperCallMe
方法,因为刚才已经说过这个方法是禁止调用的,如果非要调用将导致崩溃。
通过分析发现,因为BaseViewController
自身没有doNotSuperCallMe
方法,所以 method swizzling 之后@selector(ext_doNotSuperCallMe)
指向了父类doNotSuperCallMe
的实现,所以调用[self ext_doNotSuperCallMe]
将执行父类中的实现而导致崩溃。如果在BaseViewController
中 override doNotSuperCallMe
方法则程序运行正常。据此,我们想到了 placeholder 方案。所干就干,修改代码如下:
+ (void)load
{
SEL originalSel = @selector(doNotSuperCallMe);
SEL swizzledSel = @selector(ext_doNotSuperCallMe);
SEL placeholderSel = @selector(placeholder_doNotSuperCallMe);
swizzleInstanceMethod(self, originalSel, swizzledSel, placeholderSel);
}
- (void)ext_doNotSuperCallMe
{
NSLog(@"self: %@, %s", self, __func__);
[self ext_doNotSuperCallMe];
}
- (void)placeholder_doNotSuperCallMe
{
NSLog(@"I am BaseViewController's default implementaion of 'doNotSuperCallMe' method, and I won't call [super doNotSuperCallMe]!");
}
运行,点击button,Crash Again!
继续分析原因,swizzleInstanceMethod
这个函数还需要修改一下:
void swizzleInstanceMethod(Class theClass, SEL originalSel, SEL swizzledSel, SEL placeholderSel)
{
Method originalMethod = class_getInstanceMethod(theClass, originalSel);
IMP originalImp = method_getImplementation(originalMethod);
Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSel);
IMP swizzledImp = method_getImplementation(swizzledMethod);
Method placeholderMethod = class_getInstanceMethod(theClass, placeholderSel);
IMP placeholderImp = method_getImplementation(placeholderMethod);
// 判断原方法是否有对应实现
if (!originalMethod || !originalImp) {
BOOL succ = class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
if (succ) {
// 此时(theClass 本身和它所有父类都没有 originalSel 的方法实现),
// 必须提供 placeholder 用于方法交换
assert(placeholderMethod && placeholderImp);
class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
}
else {
NSLog(@"something goes wrong!");
assert(0);
}
return;
}
if (class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
// 此时(theClass 本身没有 originalSel 方法的实现,而他的某个父类有),
// 如果有 placeholder,则和 placeholder 交换方法实现,否则和父类交换方法实现
if (placeholderMethod && placeholderImp) {
class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
}
else {
class_replaceMethod(theClass, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
}
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
运行,点击button,OK!大功告成!
现在这个 swizzleInstanceMethod
基本上已经非常完善了,它引入了 placeholder 参数,让你在 method swizzling 的时候可以不受原类的对应方法是否实现的影响,极大地保证了代码的健壮性。这个 placeholder 也不是必须要传的,上例中,如果UIViewController
的doNotSuperCallMe
方法可以调用,则 placeholder 就可以传 nil;如果你非要传 placeholder,则将placeholder_doNotSuperCallMe
的实现进行如下修改也是可以的:
- (void)placeholder_doNotSuperCallMe
{
[super doNotSuperCallMe];
}
所以,这个 placeholder 是很灵活的。
最后,我再大概说一下 method swizzling 的基本原理。
当通过一个对象调用它的某个方法时,其实是先要找到这个方法的函数指针,通过这个函数指针再去找对应的实现。如果找不到这个函数指针,或者这个函数指针没有对应的实现,程序都将崩溃,当然OC也提供了一些消息转发机制保证程序继续正常运行,这个按下不表。所以 method swizzling 的基本原理总结下来就一句话:通过改变函数指针的指向来实现方法交换。
现在请大家思考一个问题:假如某个类有一个方法 A,而这个类有n个分类,每个分类都有一个对应的方法 An,并且所有的方法 An 都分别和方法 A 进行了 method swizzling,请问最后这些方法的调用顺序是怎样的。
如果你理解了 method swizzling 的基本原理,这个问题应该不难。欢迎大家留言解答^_^